/*
Copyright (C) 2012 Brady Vidovic

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package xmltvdb.tools;
import java.io.IOException;
import btv.http.*;
import btv.tools.BTVStringCompressor;
import java.io.FileOutputStream;
import java.io.File;
import java.util.Map;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import java.util.zip.ZipEntry;
import java.util.Enumeration;
import java.util.zip.ZipFile;
import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.io.IOUtils;
import org.jdom.Document;
import static xmltvdb.tools.Constants.*;
import static btv.tools.BTVTools.*;

/**
 * 
 * @author Brady Vidovic
 */
public class CachedHTTP extends BTVHTTP
{
    public CachedHTTP()
    {        
        super(true);    
        cleanCache();
    }
    
    
        
    
    public int uncachedQueries = 0, cachedQueries = 0;
    public long httpTotalBytes=0;
    long memoryCachedSize = 0;
    public Document getXMLFromURL(String encodedUrl)
    {
        return getXMLFromURL(encodedUrl, null);//no zip expected
    }
    
    public Document getXMLFromURL(String encodedUrl, String fileInZip)
    {
        int attempts = 0;
        while(true)
        {
            try
            {                
                if(++attempts > Config.HTTP_REQUEST_RETRIES)
                {
                    Logger.ERROR( "Failed to get XML after "+ attempts +" attempts. Wil not try again for URL: " +encodedUrl);
                    break;
                }
                return _getXMLFromURL(encodedUrl,fileInZip);            
            }
            catch(Exception x)
            {                                                
                
                Logger.WARN( "Failed to get XML (attempt "+ attempts +" of "+ Config.HTTP_REQUEST_RETRIES+") from: "+ encodedUrl 
                        +LINE_BRK+"Will try again in "+ Config.WAIT_SECOND_BETWEEN_RETIRES +" seconds",x);

                try {
                    Thread.sleep(Config.WAIT_SECOND_BETWEEN_RETIRES * 1000);
                } catch (InterruptedException ex) {
                    //ignored
                }                
            }
        }
        return null;
    }
    private AtomicInteger cachedFileName = new AtomicInteger(1);
    public Map<String,File> cachedRequestFiles = new HashMap<>();    
    public Map<String,byte[]> cachedRequests = new HashMap<>();    
    
    private synchronized Document _getXMLFromURL(String encodedUrl, String fileInZip) throws Exception
    {
        String lcUrl = encodedUrl.toLowerCase();//key for maps is case-insensitive
        //first check cache
        File cachedXmlFile;        
        if(Config.CACHE_TO_DISK)
        {
            cachedXmlFile = cachedRequestFiles.get(lcUrl);//case-insensitive

            if(cachedXmlFile != null && cachedXmlFile.exists())
            {
                Logger.DEBUG( "Getting URL from cached file ("+cachedXmlFile.getAbsolutePath()+"): "+ encodedUrl);
                cachedQueries++;
                return getXMLFromFile(cachedXmlFile);
            }
        }
        else//check memory cache
        {
            byte[] cachedBytes = cachedRequests.get(lcUrl);            
            if(cachedBytes != null){
                Logger.DEBUG( "Getting URL from cache: "+ encodedUrl);
                cachedQueries++;
                String xmlSource = BTVStringCompressor.decompress(cachedBytes);
                return getDocumentFromString(xmlSource);                
            }
        }
        
        
        //no cache avail, query the url and cache the results
        uncachedQueries++;
        
        File response = new File(CACHE_DIR, "BTVHTTP.temp."+getInstanceId()+".http");        
        try{
            
            IOUtils.copy(GET(encodedUrl),new FileOutputStream(response));
            //track total size downloaded
            long fileBytes = response.length(); 
            if(fileBytes != 0L && fileBytes > 0)
                httpTotalBytes += fileBytes;
        }catch(HttpFileNotFoundException fnf){
            //404 should be just a warning, not an error
            Logger.WARN("404 - No file found at: "+encodedUrl);
            return null;
        }catch(Exception x){
            Logger.ERROR("Failed to get data from: "+ encodedUrl,x);
            return null;//null means end lookup, trying again probably wont help
        }
                        
        
        //if its a zip extract and get the xml that has the same filename as the zip                
        if(isZipFile(response))
        {               
            List<String> foundInZip = new ArrayList<>();        
            File unzippedFile;
            try (ZipFile zipFile = new ZipFile(response, ZipFile.OPEN_READ)) 
            {
                Enumeration zipFileEntries = zipFile.entries();
                unzippedFile = null;
                while (zipFileEntries.hasMoreElements()) 
                {                
                    ZipEntry entry = (ZipEntry) zipFileEntries.nextElement();

                    String currentEntry = entry.getName();
                    foundInZip.add(currentEntry);
                    if(currentEntry.equalsIgnoreCase(fileInZip))
                    {//found what we're looking for           
                        
                        //decompress it to temp file
                        unzippedFile = new File(CACHE_DIR,"BTVHTTP.temp."+getInstanceId()+".unzipped");
                        if(unzippedFile.exists()) unzippedFile.delete();                        
                        IOUtils.copy(zipFile.getInputStream(entry), new FileOutputStream(unzippedFile));                        
                        break;
                    }                                
                }
            }
            
            if(unzippedFile == null)
                throw new Exception("No matching xml file found in zip. Looked for: "+ fileInZip +" and found: "+ foundInZip);
            else
            {
                //successfully unzipped
                //delete the original (zipped file), keep unzipped cached
                response.delete();
                response = unzippedFile;
            }
        }
        //else nothing to unzip.. regular file
        
        Document xmlDocument;
        //cache the request
        if(Config.CACHE_TO_DISK)
        {
            //cache the decompressed xml file in the cache dir using same extension
            String ext = FilenameUtils.getExtension(response.getName());        
            cachedXmlFile = new File(CACHE_DIR,cachedFileName.getAndIncrement()+ (valid(ext) ? "."+ext:""));        
            if(cachedXmlFile.exists()){
                cachedXmlFile.delete();
                Logger.ERROR("Overwriting cached XML file. This should never happen: "+ cachedXmlFile);                
            }

            //copy to cached location
            FileUtils.copyFile(response, cachedXmlFile);                           

            //parse the XML doc from the file
            xmlDocument = getXMLFromFile(response);   
        
            //finally put file in cached map if no error thrown
            cachedRequestFiles.put(lcUrl, cachedXmlFile);//case-insensitive
            
        }
        else//cache to memory
        {
            //parse the XML doc from the temp file
            String xml = FileUtils.readFileToString(response, ENCODING_FORMAT);
            xmlDocument = getDocumentFromString(xml);
            
            //finally compress and cache the xml to memory if no error thrown
            byte[] compressedXML = BTVStringCompressor.compress(xml);
            memoryCachedSize += compressedXML.length;
            cachedRequests.put(lcUrl, compressedXML);
            Logger.DEBUG("Cached XML in memory is now: "+ toFixedDecimal((memoryCachedSize/1024.0/1024.0),2) +"  MB");
        }                
        
        //clean up... delete temp response file
        if(response.exists()) response.delete();
        
        return xmlDocument;
        
    }
    
    public final void cleanCache()
    {
        //clean up any remnants from previous runs
        try
        {            
            //clean cached HTTP request data            
            FileUtils.cleanDirectory(CACHE_DIR);            
            cachedRequests.clear();

            //clean up old xmltv files too
            for(File xmlFile: FileUtils.listFiles(XMLTVS_DIR, new String[]{"xml","XML","Xml"}, false))                    
                xmlFile.delete();
            
            //reset counters
            uncachedQueries = 0;
            cachedQueries = 0;
            httpTotalBytes = 0;
            memoryCachedSize = 0;
            
            Logger.DEBUG("Successfully cleaned out HTTP Cache");
            
        }//clean up cache dir
        catch(IOException iox)
        {
            Logger.WARN( "Failed to clean up cahched xml data",iox);
        }            
    }
    
}